package workflow

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/go-gorp/gorp"

	"github.com/ovh/cds/engine/api/application"
	"github.com/ovh/cds/engine/api/cache"
	"github.com/ovh/cds/engine/api/database/gorpmapping"
	"github.com/ovh/cds/engine/api/metrics"
	"github.com/ovh/cds/engine/api/repositoriesmanager"
	"github.com/ovh/cds/sdk"
	"github.com/ovh/cds/sdk/log"
)

// HandleVulnerabilityReport calculate vulnerability trend and save report
func HandleVulnerabilityReport(ctx context.Context, db gorp.SqlExecutor, cache cache.Store, proj *sdk.Project, nr *sdk.WorkflowNodeRun, workerReport sdk.VulnerabilityWorkerReport) error {
	var defaultBranch string
	// Get default branch
	if nr.VCSServer != "" {
		// Get vcs info to known if we are on the default branch or not
		projectVCSServer := repositoriesmanager.GetProjectVCSServer(proj, nr.VCSServer)
		client, erra := repositoriesmanager.AuthorizedClient(ctx, db, cache, proj.Key, projectVCSServer)
		if erra != nil {
			return sdk.WrapError(sdk.ErrNoReposManagerClientAuth, "HandleVulnerabilityReport> Cannot get repo client %s : %v", nr.VCSServer, erra)
		}

		b, errB := repositoriesmanager.DefaultBranch(ctx, client, nr.VCSRepository)
		if errB != nil {
			return sdk.WrapError(errB, "HandleVulnerabilityReport> Unable to get default branch")
		}
		defaultBranch = b.DisplayID
	}

	// Get report on the current node run if exist
	currentNodeRunReport, err := loadVulnerabilityReport(db, nr.ID)
	if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
		return sdk.WrapError(err, "Unable to load vulnerability report")
	}

	if err != nil && sdk.ErrorIs(err, sdk.ErrNotFound) {
		if err := createNewVulnerabilityReport(db, cache, proj, nr, workerReport, defaultBranch); err != nil {
			return sdk.WrapError(err, "Unable to create no vulnerability report")
		}
	}

	currentNodeRunReport.Report.Vulnerabilities = append(currentNodeRunReport.Report.Vulnerabilities, workerReport.Vulnerabilities...)
	if currentNodeRunReport.Report.Summary == nil {
		currentNodeRunReport.Report.Summary = workerReport.Summary
	} else if workerReport.Summary != nil {
		for k, v := range workerReport.Summary {
			count, ok := currentNodeRunReport.Report.Summary[k]
			if !ok {
				currentNodeRunReport.Report.Summary[k] = v
			} else {
				currentNodeRunReport.Report.Summary[k] = count + v
			}
		}
	}

	// Update report
	dbReport := dbNodeRunVulenrabilitiesReport(currentNodeRunReport)
	if err := dbReport.PostInsert(db); err != nil {
		return sdk.WrapError(err, "Unable to insert report")
	}

	// If we are on default branch, save report on application
	if defaultBranch != "" && defaultBranch == nr.VCSBranch {
		// Save vulnerabilities
		if err := application.InsertVulnerabilities(db, currentNodeRunReport.Report.Vulnerabilities, nr.ApplicationID, workerReport.Type); err != nil {
			return sdk.WrapError(err, "Unable to insert vulnerability")
		}

		// push metrics
		vulnsDBSummary, errS := application.LoadVulnerabilitiesSummary(db, nr.ApplicationID)
		if errS != nil {
			log.Error("HandleVulnerabilityReport> Unable to get summary to create metrics: %s", err)
		}
		if vulnsDBSummary != nil && errS == nil {
			metrics.PushVulnerabilities(proj.Key, nr.ApplicationID, nr.WorkflowID, nr.Number, vulnsDBSummary)
		}
	}

	return nil
}

func createNewVulnerabilityReport(db gorp.SqlExecutor, cache cache.Store, proj *sdk.Project, nr *sdk.WorkflowNodeRun, workerReport sdk.VulnerabilityWorkerReport, defaultBranch string) error {
	// Build current report
	nodeRunReport := sdk.WorkflowNodeRunVulnerabilityReport{
		WorkflowID:        nr.WorkflowID,
		ApplicationID:     nr.ApplicationID,
		Branch:            nr.VCSBranch,
		Num:               nr.Number,
		WorkflowNodeRunID: nr.ID,
		WorkflowRunID:     nr.WorkflowRunID,
		Report: sdk.WorkflowNodeRunVulnerability{
			Vulnerabilities: workerReport.Vulnerabilities,
			Summary:         workerReport.Summary,
		},
	}

	// Get summary from previous run
	previousRunReport, err := loadPreviousRunVulnerabilityReport(db, nr)
	if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
		return sdk.WrapError(err, "Unable to get previous vulnerability report")
	}
	nodeRunReport.Report.PreviousRunSummary = previousRunReport

	// Get summary from default branch
	if defaultBranch != "" && defaultBranch != nr.VCSBranch {
		defaultBranchReport, err := loadLatestRunVulnerabilityReport(db, nr, defaultBranch)
		if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
			return sdk.WrapError(err, "Unable to get default branch vulnerability report")
		}
		nodeRunReport.Report.DefaultBranchSummary = defaultBranchReport
	}

	// Flag as ignored, vulnerabilities already ignore
	var errS error
	nodeRunReport.Report.Vulnerabilities, errS = syncVunerabilitiesWithApplication(db, nodeRunReport, nr.ApplicationID)
	if errS != nil {
		return sdk.WrapError(errS, "HandleVulnerabilityReport> Unable to sync vunerabilities")
	}

	if err := InsertVulnerabilityReport(db, nodeRunReport); err != nil {
		return sdk.WrapError(err, "Unable to save vulnerability report")
	}

	// If we are on default branch, save report on application
	if defaultBranch != "" && defaultBranch == nr.VCSBranch {
		if err := application.InsertVulnerabilities(db, nodeRunReport.Report.Vulnerabilities, nr.ApplicationID, workerReport.Type); err != nil {
			return sdk.WrapError(err, "Unable to update vulnerability")
		}
	}

	return nil
}

func syncVunerabilitiesWithApplication(db gorp.SqlExecutor, nodeRunReport sdk.WorkflowNodeRunVulnerabilityReport, appID int64) ([]sdk.Vulnerability, error) {
	appVuln, err := application.LoadVulnerabilities(db, appID)
	if err != nil {
		return nil, sdk.WrapError(err, "Unable to load application vulnerabilities")
	}

	// create map
	m := make(map[string]sdk.Vulnerability, len(nodeRunReport.Report.Vulnerabilities))
	for _, v := range nodeRunReport.Report.Vulnerabilities {
		m[fmt.Sprintf("%s-%s-%s", v.Component, v.Version, v.CVE)] = v
	}

	for _, v := range appVuln {
		if v.Ignored {
			mVuln, ok := m[fmt.Sprintf("%s-%s-%s", v.Component, v.Version, v.CVE)]
			if !ok {
				continue
			}
			mVuln.Ignored = true
			m[fmt.Sprintf("%s-%s-%s", v.Component, v.Version, v.CVE)] = mVuln
		}
	}

	result := make([]sdk.Vulnerability, len(m))
	i := 0
	for _, v := range m {
		result[i] = v
		i++
	}
	return result, nil
}

func loadPreviousRunVulnerabilityReport(db gorp.SqlExecutor, nr *sdk.WorkflowNodeRun) (map[string]int64, error) {
	var dbReport dbNodeRunVulenrabilitiesReport
	query := `
    SELECT * FROM workflow_node_run_vulnerability
    WHERE application_id = $1 AND workflow_id = $2 AND branch = $3 AND workflow_number < $4
    ORDER BY workflow_number DESC
    LIMIT 1
  `
	if err := db.SelectOne(&dbReport, query, nr.ApplicationID, nr.WorkflowID, nr.VCSBranch, nr.Number); err != nil {
		if err == sql.ErrNoRows {
			return nil, sdk.WithStack(sdk.ErrNotFound)
		}
		return nil, sdk.WrapError(err, "Unable to load previous report")
	}
	return dbReport.Report.Summary, nil
}

func loadLatestRunVulnerabilityReport(db gorp.SqlExecutor, nr *sdk.WorkflowNodeRun, branch string) (map[string]int64, error) {
	var dbReport dbNodeRunVulenrabilitiesReport
	query := `
    SELECT * FROM workflow_node_run_vulnerability
    WHERE application_id = $1 AND workflow_id = $2 AND branch = $3
    ORDER BY workflow_number DESC, workflow_node_run_id DESC
    LIMIT 1
  `
	if err := db.SelectOne(&dbReport, query, nr.ApplicationID, nr.WorkflowID, branch); err != nil {
		if err == sql.ErrNoRows {
			return nil, sdk.WithStack(sdk.ErrNotFound)
		}
		return nil, sdk.WrapError(err, "Unable to load latest report")
	}
	return dbReport.Report.Summary, nil
}

// InsertVulnerabilityReport inserts vulnerability report
func InsertVulnerabilityReport(db gorp.SqlExecutor, report sdk.WorkflowNodeRunVulnerabilityReport) error {
	dbReport := dbNodeRunVulenrabilitiesReport(report)
	if err := db.Insert(&dbReport); err != nil {
		return sdk.WrapError(err, "Unable to insert report")
	}
	return nil
}

// PostGet  is a db hook
func (d *dbNodeRunVulenrabilitiesReport) PostGet(db gorp.SqlExecutor) error {
	var reportS sql.NullString
	query := "SELECT report from workflow_node_run_vulnerability WHERE id = $1"
	if err := db.QueryRow(query, d.ID).Scan(&reportS); err != nil {
		return sdk.WrapError(err, "Unable to report")
	}

	var report sdk.WorkflowNodeRunVulnerability
	if err := gorpmapping.JSONNullString(reportS, &report); err != nil {
		return sdk.WrapError(err, "Unable to unmarshal report")
	}

	d.Report = report
	return nil
}

// PostInsert  is a db hook
func (d *dbNodeRunVulenrabilitiesReport) PostInsert(db gorp.SqlExecutor) error {
	report, err := gorpmapping.JSONToNullString(d.Report)
	if err != nil {
		return sdk.WrapError(err, "Unable to marshal report")
	}
	query := "UPDATE workflow_node_run_vulnerability set report=$1 WHERE id=$2"
	if _, err := db.Exec(query, report, d.ID); err != nil {
		return sdk.WrapError(err, "Unable to insert report")
	}
	return nil
}

func loadVulnerabilityReport(db gorp.SqlExecutor, nodeRunID int64) (sdk.WorkflowNodeRunVulnerabilityReport, error) {
	var dbReport dbNodeRunVulenrabilitiesReport
	query := `
    SELECT * FROM workflow_node_run_vulnerability
		WHERE workflow_node_run_id = $1
		ORDER BY id DESC
		LIMIT 1
  `
	if err := db.SelectOne(&dbReport, query, nodeRunID); err != nil {
		if err == sql.ErrNoRows {
			return sdk.WorkflowNodeRunVulnerabilityReport{}, sdk.ErrNotFound
		}
		return sdk.WorkflowNodeRunVulnerabilityReport{}, sdk.WrapError(err, "Unable to load vulnerability report for node run %d", nodeRunID)
	}
	return sdk.WorkflowNodeRunVulnerabilityReport(dbReport), nil
}
